package org.checkerframework.checker.regex;

import javax.lang.model.element.AnnotationMirror;
import javax.lang.model.element.ExecutableElement;
import org.checkerframework.dataflow.analysis.ConditionalTransferResult;
import org.checkerframework.dataflow.analysis.TransferInput;
import org.checkerframework.dataflow.analysis.TransferResult;
import org.checkerframework.dataflow.cfg.node.ClassNameNode;
import org.checkerframework.dataflow.cfg.node.GreaterThanNode;
import org.checkerframework.dataflow.cfg.node.GreaterThanOrEqualNode;
import org.checkerframework.dataflow.cfg.node.IntegerLiteralNode;
import org.checkerframework.dataflow.cfg.node.LessThanNode;
import org.checkerframework.dataflow.cfg.node.LessThanOrEqualNode;
import org.checkerframework.dataflow.cfg.node.MethodAccessNode;
import org.checkerframework.dataflow.cfg.node.MethodInvocationNode;
import org.checkerframework.dataflow.cfg.node.Node;
import org.checkerframework.dataflow.expression.JavaExpression;
import org.checkerframework.dataflow.util.NodeUtils;
import org.checkerframework.framework.flow.CFAbstractAnalysis;
import org.checkerframework.framework.flow.CFStore;
import org.checkerframework.framework.flow.CFTransfer;
import org.checkerframework.framework.flow.CFValue;
import org.checkerframework.javacutil.ElementUtils;
import org.checkerframework.javacutil.TreeUtils;

/** The transfer function for the Regex Checker. */
public class RegexTransfer extends CFTransfer {

  // isRegex and asRegex are tested as signatures (string name plus formal parameters), not
  // ExecutableElement, because they exist in two packages:
  // org.checkerframework.checker.regex.util.RegexUtil.isRegex(String,int)
  // org.plumelib.util.RegexUtil.isRegex(String,int)
  // and org.plumelib.util might not be on the classpath.
  private static final String IS_REGEX_METHOD_NAME = "isRegex";
  private static final String AS_REGEX_METHOD_NAME = "asRegex";

  /** The MatchResult.groupCount() method. */
  private final ExecutableElement matchResultgroupCount;

  /** Create the transfer function for the Regex Checker. */
  public RegexTransfer(CFAbstractAnalysis<CFValue, CFStore, CFTransfer> analysis) {
    super(analysis);
    this.matchResultgroupCount =
        TreeUtils.getMethod(
            "java.util.regex.MatchResult",
            "groupCount",
            0,
            analysis.getTypeFactory().getProcessingEnv());
  }

  // TODO: These are special cases for isRegex(String, int) and asRegex(String, int).  They should
  // be replaced by adding an @EnsuresQualifierIf annotation that supports specifying attributes.
  @Override
  public TransferResult<CFValue, CFStore> visitMethodInvocation(
      MethodInvocationNode n, TransferInput<CFValue, CFStore> in) {
    TransferResult<CFValue, CFStore> result = super.visitMethodInvocation(n, in);

    // refine result for some helper methods
    MethodAccessNode target = n.getTarget();
    ExecutableElement method = target.getMethod();
    Node receiver = target.getReceiver();
    if (receiver instanceof ClassNameNode) {
      ClassNameNode cnn = (ClassNameNode) receiver;
      String receiverName = cnn.getElement().toString();
      if (isRegexUtil(receiverName)) {
        result = handleRegexUtil(n, method, result);
      }
    }
    return result;
  }

  private TransferResult<CFValue, CFStore> handleRegexUtil(
      MethodInvocationNode n, ExecutableElement method, TransferResult<CFValue, CFStore> result) {
    RegexAnnotatedTypeFactory factory = (RegexAnnotatedTypeFactory) analysis.getTypeFactory();
    if (ElementUtils.matchesElement(method, IS_REGEX_METHOD_NAME, String.class, int.class)) {
      // RegexUtil.isRegex(s, groups) method
      // (No special case is needed for isRegex(String) because of
      // the annotation on that method's definition.)

      CFStore thenStore = result.getRegularStore();
      CFStore elseStore = thenStore.copy();
      ConditionalTransferResult<CFValue, CFStore> newResult =
          new ConditionalTransferResult<>(result.getResultValue(), thenStore, elseStore);
      JavaExpression firstParam = JavaExpression.fromNode(n.getArgument(0));

      // add annotation with correct group count (if possible,
      // regex annotation without count otherwise)
      Node count = n.getArgument(1);
      int groupCount;
      if (count instanceof IntegerLiteralNode) {
        IntegerLiteralNode iln = (IntegerLiteralNode) count;
        groupCount = iln.getValue();
      } else {
        groupCount = 0;
      }
      AnnotationMirror regexAnnotation = factory.createRegexAnnotation(groupCount);
      thenStore.insertValue(firstParam, regexAnnotation);
      return newResult;
    } else if (ElementUtils.matchesElement(method, AS_REGEX_METHOD_NAME, String.class, int.class)) {
      // RegexUtil.asRegex(s, groups) method
      // (No special case is needed for asRegex(String) because of
      // the annotation on that method's definition.)

      // add annotation with correct group count (if possible,
      // regex annotation without count otherwise)
      AnnotationMirror regexAnnotation;
      Node count = n.getArgument(1);
      int groupCount;
      if (count instanceof IntegerLiteralNode) {
        IntegerLiteralNode iln = (IntegerLiteralNode) count;
        groupCount = iln.getValue();
      } else {
        groupCount = 0;
      }
      regexAnnotation = factory.createRegexAnnotation(groupCount);
      return recreateTransferResult(regexAnnotation, result);
    }
    return result;
  }

  @Override
  public TransferResult<CFValue, CFStore> visitLessThan(
      LessThanNode n, TransferInput<CFValue, CFStore> in) {
    // Look for: constant < mat.groupCount()
    // Make mat be @Regex(constant + 1)
    TransferResult<CFValue, CFStore> res = super.visitLessThan(n, in);
    return handleMatcherGroupCount(n.getRightOperand(), n.getLeftOperand(), false, res);
  }

  @Override
  public TransferResult<CFValue, CFStore> visitLessThanOrEqual(
      LessThanOrEqualNode n, TransferInput<CFValue, CFStore> in) {
    // Look for: constant <= mat.groupCount()
    // Make mat be @Regex(constant)
    TransferResult<CFValue, CFStore> res = super.visitLessThanOrEqual(n, in);
    return handleMatcherGroupCount(n.getRightOperand(), n.getLeftOperand(), true, res);
  }

  @Override
  public TransferResult<CFValue, CFStore> visitGreaterThan(
      GreaterThanNode n, TransferInput<CFValue, CFStore> in) {

    TransferResult<CFValue, CFStore> res = super.visitGreaterThan(n, in);
    return handleMatcherGroupCount(n.getLeftOperand(), n.getRightOperand(), false, res);
  }

  @Override
  public TransferResult<CFValue, CFStore> visitGreaterThanOrEqual(
      GreaterThanOrEqualNode n, TransferInput<CFValue, CFStore> in) {
    // Look for: mat.groupCount() >= constant
    // Make mat be @Regex(constant)
    TransferResult<CFValue, CFStore> res = super.visitGreaterThanOrEqual(n, in);
    return handleMatcherGroupCount(n.getLeftOperand(), n.getRightOperand(), true, res);
  }

  /**
   * If possibleMatcher is a call of groupCount on a Matcher and possibleConstant is a constant,
   * annotate the matcher as constant + 1 if !isAlsoEqual constant if isAlsoEqual.
   *
   * @param possibleMatcher the Node that might be a call of Matcher.groupCount()
   * @param possibleConstant the Node that might be a constant
   * @param isAlsoEqual if true, the comparison operation is strict; if false, the comparison is
   *     reflexive
   * @param resultIn the TransferResult
   * @return the possibly refined output TransferResult
   */
  private TransferResult<CFValue, CFStore> handleMatcherGroupCount(
      Node possibleMatcher,
      Node possibleConstant,
      boolean isAlsoEqual,
      TransferResult<CFValue, CFStore> resultIn) {
    if (!(possibleMatcher instanceof MethodInvocationNode)) {
      return resultIn;
    }
    if (!(possibleConstant instanceof IntegerLiteralNode)) {
      return resultIn;
    }

    if (!NodeUtils.isMethodInvocation(
        possibleMatcher, matchResultgroupCount, analysis.getTypeFactory().getProcessingEnv())) {
      return resultIn;
    }

    MethodAccessNode methodAccessNode = ((MethodInvocationNode) possibleMatcher).getTarget();
    Node receiver = methodAccessNode.getReceiver();

    JavaExpression matcherReceiver = JavaExpression.fromNode(receiver);

    IntegerLiteralNode iln = (IntegerLiteralNode) possibleConstant;
    int groupCount;
    if (isAlsoEqual) {
      groupCount = iln.getValue();
    } else {
      groupCount = iln.getValue() + 1;
    }

    CFStore thenStore = resultIn.getRegularStore();
    CFStore elseStore = thenStore.copy();
    ConditionalTransferResult<CFValue, CFStore> newResult =
        new ConditionalTransferResult<>(resultIn.getResultValue(), thenStore, elseStore);
    RegexAnnotatedTypeFactory factory = (RegexAnnotatedTypeFactory) analysis.getTypeFactory();

    AnnotationMirror regexAnnotation = factory.createRegexAnnotation(groupCount);
    thenStore.insertValue(matcherReceiver, regexAnnotation);

    return newResult;
  }

  /**
   * Returns true if the given receiver is a class named "RegexUtil". Examples of such classes are
   * org.checkerframework.checker.regex.util.RegexUtil and org.plumelib.util.RegexUtil, and the user
   * might copy one into their own project.
   *
   * @param receiver some string
   * @return true if the given receiver is a class named "RegexUtil"
   */
  private boolean isRegexUtil(String receiver) {
    return receiver.equals("RegexUtil") || receiver.endsWith(".RegexUtil");
  }
}
